Skip to content

fix(metadata-protocol)!: history and rollback doors refuse a bare two-holder name and address one holder by ?package= - #33193

Merged
objectstack-fleet[bot] merged 20 commits into
mainfrom
claude/project-thread-9jncd7
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 20 commits into
mainfrom
claude/project-thread-9jncd7

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Requested by Tesla · project thread

Fixes #32905

Clause-②: yes (narrowing)
Metadata-protocol: added
Skills-docs: none — no skill under skills/ names the history or rollback door, and the docs that do (content/docs/api/metadata-api.mdx, content/docs/kernel/contracts/metadata-service.mdx, content/docs/plugins/adding-a-metadata-type.mdx, content/docs/permissions/system-context.mdx) list the routes without teaching how a name two packages hold is addressed, so no sentence there turns false; the generated API reference (content/docs/references/api/protocol.mdx) carries the new packageId field.

Levels follow the metadata protocol alone (maintainer ruling A, 2026-10-21): minor for @objectstack/spec, the one package that carries the added protocol surface (HistoryMetaItemRequestSchema.packageId, the authorable-surface entry api/HistoryMetaItemRequest:packageId, the new RollbackMetaItemRequestSchema that declares the rollback door's request with its packageId and a toVersion of at least 1, and PACKAGE_SLOTTED_METADATA_TYPES / isPackageSlottedMetaType, the one list of types stored per package that the doors and the console both read), and the level Check Changeset demands for Metadata-protocol: added. Every other package is patch under the BREAKING banner above: @objectstack/metadata-protocol (historyMetaItem and RollbackMetaItemRequest take packageId), @objectstack/rest (both doors accept ?package=; the rollback door reads toVersion through its declaration), @objectstack/metadata (new exports historyRowAttribution, ENVIRONMENT_HISTORY_MARK; PACKAGE_SLOTTED_STORE_TYPES is now built from the spec list, same members), @object-ui/data-objectstack (history() / rollback() take packageId), @objectstack/metadata-core (a field description only) and @object-ui/app-shell (no exported signature changes: HistoryPanel and StudioItemTools are not exported, and MetadataResourceHistoryPage keeps its props).

维护者速读

  • 改了什么:同名对象由两个包各持一份时(flow、webhook 等按包存储的类型),History 和回滚不再把两家的版本混在一起;不带包的请求在普通读取会拒绝的地方一律返回 409 并列出各家(包括只在代码里提供该名字的包),带 ?package= 只读、只回滚那一家的版本。非按包存储的类型带 ?package= 返回 400。控制台的 History 工具会带上条目页的包。
  • 为什么改:实测发现,不带包的回滚会把 beta 的版本写进 alpha 的行(静默选中一家),历史列表把两家的版本当成同一条记录。
  • 风险与回滚:只有“两家同时持有同名”时行为才变(原来 200,现在 409);只有一家持有时不带包的历史和回滚与 main 完全一致(包括包接管了环境行之后回滚到环境版本)。回滚方式是直接 revert 这个 PR,不涉及数据迁移。
  • 先看:packages/metadata-protocol/src/protocol-history-slots.ts(判定规则)和 dogfood 测试 history-rollback-two-holders.dogfood.test.ts。
  • 第四轮补了控制台:对象、视图、页面这类非按包存储的条目,History 不再带包名(服务端对它们带包名返回 400),按包存储的类型清单只在 spec 里维护一份。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=1 test/history-rollback-two-holders.dogfood.test.ts,11 条全绿。
  • 需要你做的:无,座位按常规流程审核合入。

What changed

The rule (historySlotOf, new protocol-history-slots.ts), for a type stored per (package, name) (PACKAGE_SLOTTED_STORE_TYPES), is the one DatabaseLoader already keeps for MetadataManager.getHistory / rollback:

  • Bare (no ?package=): refused 409 RESOURCE_CONFLICT wherever the bare plain read refuses the name, before any row is read or written: a name whose active rows are held by two or more row slots (two packages', or a package's beside the package-less row; ambiguous-in-environment, details.candidates naming every holder), a name one row holds while a second package ships it in code, and a name two packages ship with no stored row (the registry's resolveItem, the resolver the plain read asks, throws the read's own refusal, ADR-0150 D5). One slot or none: answers exactly as on main, its whole log, and a bare rollback restores any of its versions with no ownership check (so a name saved package-less and later taken over by a package still rolls back to its environment versions, the M1 case).
  • ?package=P (the /layers?package= spelling): the slot of the row a read naming P serves (findServedOverlayRow with servedOverride: P's own row, its .custom pair's declared override of P, or the package-less row that read falls back to), else P's own slot when P ships the name. A package that neither holds nor ships the name is refused 409 not-held-by-qualifier while another slot holds it; with no row stored, a rollback naming a package that is not installed is refused 422 WRITABLE_PACKAGE_REQUIRED (the uninstalledNamedBaseRefusal a save gets) instead of minting a slot.
  • History lists that slot's versions only. An addressed rollback restores into that slot's row, and a version of another slot is refused 404 VERSION_NOT_FOUND saying whose it is ("Nothing was restored.").
  • A version with no attribution (recorded before this release), or attributed to the environment, belongs to a slot only while the name has no other slot: never guessed with two holders.
  • Every other type: ?package= stays refused 400 as on main, now VALIDATION_ERROR (param: package) saying why, on both doors before either door's early answers; never accepted and dropped.

Attribution. SysMetadataRepository (the /meta doors' store) now writes sys_metadata_history.package_id for by-package types, as DatabaseLoader does (sys_metadata = the package-less row). @objectstack/metadata exports that rule as historyRowAttribution / ENVIRONMENT_HISTORY_MARK so both writers read one rule.

Doors. GET /meta/:type/:name/history and POST /meta/:type/:name/rollback declare package in their closed query sets (META_ITEM_HISTORY_PARAMS, META_ITEM_ROLLBACK_PARAMS), refuse it repeated, fold all/empty to no package (metaItemPackageBinding), and render the refusal with details.candidates (packageScopedCandidates, as the plain read does). The rollback door's request is now declared in spec (RollbackMetaItemRequestSchema, toVersion a whole number of at least 1), beside HistoryMetaItemRequestSchema.packageId, and the door reads toVersion (body, version alias and query) through that declaration in place of the undeclared whole-number parse, so the published contract and the door spell a version one way.

Console. MetadataClient.history() / rollback() take packageId (sent as ?package=); the History tool sends its item page's ?package= (routed page), the edit page's history sheet sends ownerPackageId (the package its layered read uses), and Studio's item tools send the packageId its Layers read already uses — each only for a type stored per package. HistoryPanel asks isPackageSlottedMetaType (@objectstack/spec/api) before naming the package, so an object, view or page opened under a package reads and restores its history bare, as on main (round 4, record 6878813627 M1: the doors refuse ?package= 400 on those types). The by-package type list lives once in spec, PACKAGE_SLOTTED_METADATA_TYPES; @objectstack/metadata's PACKAGE_SLOTTED_STORE_TYPES is built from it, and protocol-history-slots.test.ts holds the doors' refusal and the console's predicate to the same list.

Measurement (before the fix)

Posted on the card: 6867256997. On a booted showcase with two AppPlugin packages each holding flow qa_hist_shared: bare history 200 interleaving both holders' versions; ?package= 400 (not supported); bare rollback {toVersion: 2} (beta's) 200 and wrote beta's body into alpha's row — a silent pick. Seat regrade: stays p2 (triage 6867291698).

After the fix (same boot, history-rollback-two-holders.dogfood.test.ts)

Request Before After
bare GET …/history, two holders 200, both holders' versions 409, candidates both pairs
bare POST …/rollback, two holders 200, writes into one holder 409, candidates both, neither row changes
GET …/history?package=alpha.custom 400 200, alpha's versions only (legacy unattributed excluded)
POST …/rollback?package=alpha.custom to beta's version 400 404, "belongs to package 'beta.custom'", nothing restored
POST …/rollback?package=alpha.custom to an unattributed version 400 404, "attributed to no package … more than one holds the name"
POST …/rollback?package=alpha.custom to its own version 400 200, only alpha's row changes
?package=com.qa.hist32905.alpha (the base) 400 addresses its pair's declared override, both doors
?package= a stranger package 400 409 not-held-by-qualifier
one row held, a second package ships the name, bare history / rollback 200 409, the bare plain read's own refusal and candidates; the row is unchanged
two packages ship the name, no row stored, bare history / rollback 200 (empty log) / 404 409, the bare plain read's candidates
?package= on view (not stored per package), both doors 400 (unknown parameter) 400 VALIDATION_ERROR, saying the type is not stored per package (r1 of this PR had accepted and dropped it)
no row stored, rollback?package= an uninstalled package 200 422 WRITABLE_PACKAGE_REQUIRED; ?package= the installed pair restores (200)
one-holder name, bare (CONTROL) 200 200, whole log, bare rollback works
name saved package-less, taken over by a package (CONTROL, M1) 200 200: bare rollback to the environment v1, and ?package= history lists and restores the environment v2

Red lines

  1. Resolution is never loosened. Nothing that refused before serves now: the bare plain read, /layers and every other door are untouched; the new refusals only narrow. CONTROL: a name one package holds keeps its whole log and its bare rollback, as before (dogfood), CONTROL — no package, 'all' and the empty string send no 'packageId' (REST), CONTROL: no 'packageId' sends no 'package' key on either door (client) and CONTROL: no '?package=' sends no 'packageId' on either door (console) hold the one-holder and bare spellings unchanged; rest-server-meta-member-selector.test.ts stays green with only its measured-set pin taking package.
  2. Ambiguity is reported, never guessed. the bare history of the two-holder name is refused 409 naming both holders, the bare rollback of the two-holder name is refused 409 naming both, and neither holder's row changes, ?package= restores only that holder's row; the other holder's version and an unattributed one are refused, writing nothing, a package that holds nothing of the name is refused naming the holders (dogfood); the protocol's two-holder refusal is a 409 naming every holder in 'details.candidates' (REST). An unattributed legacy version is never assigned to a holder while two hold the name. Round 2 adds, at the dogfood boot, a name two packages ship, only one customized, is refused bare at both doors, as the bare plain read refuses it, ?package= on a type not stored per package is refused 400 at both doors, never accepted and dropped, a rollback naming no installed package, of a name no row holds, is refused and stores nothing; an installed holder restores and CONTROL (M1): a name the environment saved and a package took over restores the environment's versions, bare and addressed, as before; in protocol-history-slots.test.ts (13 cases) each arm of historySlotOf alone, both F5 arms (P ships the name / does not, the read falls back to the package-less row: the environment's slot); and in the REST file, through the real protocol, the 400 on a view at both doors. Round 3 adds a name two packages ship and neither customized, no row stored, is refused bare at both doors… (dogfood) and RollbackMetaItemRequestSchema declares what the rollback door enforces (spec: 0, -1, 1.5, '2', true refused; 1 with every optional member preserved).
  3. Every child is independently releasable. One PR, no migration (no stored shape changes; package_id already existed), no dependency on another child's branch; the bare one-holder behaviour is unchanged so it ships alone.

Ablations

Each leg mutated one source line with scripts/ablation-replace.mjs (anchor must hit once; blob change proved; restore to HEAD proved), rebuilt @objectstack/metadata-protocol, proved the mutation live in dist/ with scripts/ablation-dist-preflight.mjs (exit 0, tree clean), and ran history-rollback-two-holders.dogfood.test.ts. L1, L2 and L5 ran on r1 and their anchors are unchanged; L3 and L4 were re-anchored onto the rewritten isVersionOf (L3b, L4b) and re-run on this head with L6–L10. After the last leg the rebuild was proved clean: ablation-dist-preflight … __ablate32905 --absent exit 0 (22 built files), tree clean, and the file is 11/11 green.

Leg Mutation (forbidden behaviour put back) Red (first failure)
L1 history skips historySlotOf when no package is named the bare history of the two-holder name is refused 409 naming both holders: expected 200 to be 409 (also the base-package case's bare-409 line)
L2 rollback skips historySlotOf when no package is named the bare rollback … neither holder's row changes: {"success":true,…"restoredFromVersion":1} expected 200 to be 409; the cascade reddens the two ?package= cases (the bare write grew alpha's log: expected [ 1, 3, 6 ] to have a length of 2)
L3b isVersionOf accepts another holder's attributed version ?package= reads that holder's versions alone…: expected [ 1, 2, 3, 4 ] to have a length of 2 but got 4; ?package= restores only that holder's row…: expected 200 to be 404; also the base-package case
L4b isVersionOf accepts an unattributed version under two holders ?package= reads…: expected [ 1, 3, 5 ] to have a length of 2 but got 3; ?package= restores…: expected 200 to be 404; also the stranger-package case
L5 the not-held-by-qualifier refusal disabled a package that holds nothing of the name is refused naming the holders: {"events":[]} expected 200 to be 409
L6 bare doors skip the bare plain read's judgement (F3) a name two packages ship, only one customized, is refused bare at both doors…: bare history expected 200 to be 409
L7 ?package= on a type not stored per package let through (F4) ?package= on a type not stored per package is refused 400 at both doors…: {"events":[]} expected 200 to be 400
L8 the uninstalled-package refusal skipped for an unheld slot (F6) a rollback naming no installed package, of a name no row holds, is refused…: expected 404 to be 422
L9 an environment-attributed version no longer belongs to a sole slot (F2, M1) CONTROL (M1) … restores the environment's versions, bare and addressed: expected [ 4, 5 ] to include 2
L10 the bare rollback checks ownership as if the slot were shared (F2) CONTROL (M1) …: bare rollback to the environment v1 expected 404 to be 200
M1 HistoryPanel names the package for every type again (addressedPackage = packageId) 4 of 14 red: a type not stored per package reads and restores bare under ?package=… (history called with packageId), Studio's data and interfaces pillars (object, page history with packageId), and Studio's object rollback
N1 RollbackMetaItemRequestSchema.toVersion loosened back to z.number() (spec rebuilt, marker proved in packages/spec/dist) rest-server-query-number-census.test.ts, 8 of 35 red: body { toVersion: 1.5 }, 0, -1, the version alias 1.5 / 0, and ?toVersion="1.5" / "0" / "-1" now reach the protocol instead of answering 400 — the door reads the declaration

Verification

Round 4, on head 248bc6bec (after one more merge of origin/main), each command run with </dev/null and its exit code landed on disk first:

  • dispatch-gates --commands: 123 commands, all exit 0; dispatch-gates --ran: 123 derived, 123 run, 0 NOT-MEASURED.
  • pnpm lint: exit 0.
  • Typecheck: @objectstack/metadata-protocol, rest, spec, dogfood exit 0; rest check:test-typecheck exit 0; @object-ui/data-objectstack and @object-ui/app-shell type-check exit 0 (after building their dependency closure).
  • Suites: metadata-protocol 340 files / 22858 passed; rest 415 / 8671; spec 931 / 28396; metadata 77 / 2314 (skips pre-existing, none added).
  • Full dogfood battery: 465 files passed, 3 skipped; 3939 tests passed, 42 skipped.
  • UI: data-objectstack 120 files / 1386 passed; app-shell item-tools, Studio item-tools and ResourceEditPage 65 files / 312 passed.
  • check:generated (spec): exit 0 after --fix regenerated api-surface and export-origins for the two new exports.

Acceptance notes

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5

… of a by-package name

WIP: the /meta history and rollback doors take ?package=, a bare request
of a name two row slots hold is refused naming both, and the repository
records whose row each history snapshot is of.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
…ress one holder by package

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
…he holder refusal with candidates

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/metadata, @objectstack/rest, @objectstack/spec, touching 87 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

56 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json b63c894934b5e427b456b997774f48a220edb7e5.

⛔ 17 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: /meta/:type (route, 43 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 178 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b63c894934b5e427b456b997774f48a220edb7e5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1452deeba305b43e2116e76a672a51450492be6 — the merge of head 952550aea38aba428e045206b01aedc104f49090 into base b63c894934b5e427b456b997774f48a220edb7e5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a1452deeba305b43e2116e76a672a51450492be6 && git checkout a1452deeba305b43e2116e76a672a51450492be6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b63c894934b5e427b456b997774f48a220edb7e5 952550aea38aba428e045206b01aedc104f49090 && git checkout -B drift-repro b63c894934b5e427b456b997774f48a220edb7e5 && git merge --no-ff 952550aea38aba428e045206b01aedc104f49090

node scripts/docs-audit/affected-docs.mjs --json b63c894934b5e427b456b997774f48a220edb7e5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b63c894934b5e427b456b997774f48a220edb7e5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibzECS2npZFku4956Um

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1c1a28f9ac0f7197c2049a2f7d51c3542c6c4e3b
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/rest/src/meta-item-history-routes.ts, packages/rest/src/rest-server.ts, packages/metadata-protocol/src/protocol-history-slots.ts, packages/metadata-protocol/src/protocol-history-and-publish.ts, packages/metadata-protocol/src/protocol-door-requests.ts, packages/metadata-protocol/src/protocol-package-commits.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/sys-metadata-repository.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/index.ts, packages/metadata-core/src/objects/sys-metadata-history.object.ts, packages/spec/authorable-surface/api.json, ui/packages/data-objectstack/src/metadata-client.ts, ui/packages/app-shell/src/views/item-tools/ResourceHistoryPage.tsx, ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx, ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx

Read: card #32905 and its five comments (triage 6839348119, claim 6867138888, measurement 6867256997, regrade 6867282696 "stays p2", os-dev-report 6869029675); the PR body, file list and net diff against main; the check-runs on this head; ADR-0150 D3 to D5. Code outside the diff was read, not reviewed. Tests, the changeset prose and content/docs are the lane seat's ACCEPT at TIER_DEFAULT.

Check-runs on this head when judged: Check Changeset = failure (finding F1). Success: Dogfood Regression Gate, Type Check · source gates, Lint · ESLint, Lint · docs & code guards, Lint · CI, release & engine gates, the three Lint · scoped runs, Spec property liveness, Governed Surface Queue Guard, PR-body guards, Check PR Size, Check Documentation Links, Build Docs, Build UI Deps. Still running, not judged: Test Core 1/8 to 8/8, Test UI 1/16 to 16/16, Build Core, Build & E2E UI, Live E2E UI, Lint UI, Lint · PM dispatch-gates, Lint · repo & tooling gates, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Type Check UI. This verdict does not depend on them; a PASS on a later head must wait for them.

① Derived judgments

  • ?package= joins the closed sets of GET …/history and POST …/rollback (META_ITEM_HISTORY_PARAMS, META_ITEM_ROLLBACK_PARAMS), repeated values refused, all and empty folded by metaItemPackageBinding. Right for a type in PACKAGE_SLOTTED_STORE_TYPES. Wrong for every other type: the value is recognised, then dropped (F4).
  • 409 ambiguous-in-environment for a bare name two row slots hold, before any read or write. Right, and it closes the measured silent pick: historySlotOf throws ahead of the lock gate and of restoreVersion on both doors, for two packages' rows and for a package's row beside the package-less row. It judges stored active rows only, which is narrower than ADR-0150's "one holder in the environment" (F3).
  • 404 VERSION_NOT_FOUND for another slot's version, and for an unattributed version while two hold the name. Right for the two-holder case: isVersionOf runs before the body is read, and nothing is written. The same check also runs on the bare one-slot rollback, where it changes an answer main gives (F2).
  • sys_metadata_history.package_id written by SysMetadataRepository. Right: both insert sites (the put and the delete tombstone) carry it, from the binding of the row actually written; the package-less row gets the sys_metadata mark; other types leave the column out. No stored shape changes, old rows keep NULL, and a re-run rewrites nothing.
  • historyRowAttribution and ENVIRONMENT_HISTORY_MARK exported from @objectstack/metadata. Right: one rule for both writers, no behaviour change in DatabaseLoader.
  • MetadataClient.history() and rollback() take packageId. Right: sent only when truthy, so the bare spelling is byte-identical to main.
  • HistoryMetaItemRequestSchema.packageId (optional string) and its shard line api/HistoryMetaItemRequest:packageId in authorable-surface/api.json. The schema change is right, the generated shard matches it (one key, sorted in place), and protocol.mdx carries the same sentence. Its release declaration is wrong (F1).

② Semver level

  • F1 · must-fix · Check Changeset is red on this head. The diff moves packages/spec/authorable-surface/api.json (a key entered), and the PR body and changeset both declare Metadata-protocol: none with every package at patch. The gate's own verdict: declare Metadata-protocol: added and grade the package that carries it (@objectstack/spec) at least minor. The declaration goes on the PR body and in the changeset body alike. The baseline is not to be edited to quiet the gate.
  • The rest of the changeset holds: patch plus the BREAKING banner is this repository's convention for a non-protocol breaking change (the WHICH LEVEL text in pr-automation.yml: it ships patch and keeps its carriers), so rest, metadata-protocol, metadata, metadata-core and the two @object-ui packages may stay patch.
  • Clause-②: yes (narrowing) is right: the bare two-holder 200 becomes 409. The changeset sentence "A name one holder holds answers bare exactly as before" is not true of the tree (F2) and must not ship as written.
  • ADR-0087 entry: not owed. check-adr-0087-registration ran inside the same job and accepted the not-required (no-migration-prescription) marker for this changeset; no authored key is removed or re-shaped and no stored row changes shape. That stays true after F1's re-declaration (added prescribes no migration).

③ Boundary flags

  • rest-server.ts closed query sets: answered, accepted. AGENTS.md route rule 5 puts the closed set there, and the door cannot read package without it.
  • protocol.zod.ts plus api.json and protocol.mdx: accepted as surface (HistoryMetaItemRequest is the route's request type), but it is exactly what makes F1 owed. The dev report says "CI check-runs: not yet read", so the red gate was not seen.
  • sys-metadata-history.object.ts: doc comment only, accepted.
  • StudioItemTools.tsx and ResourceEditPage.tsx, one prop each: accepted as the History tool's other two mounts. The prop is what makes F5 reachable from the console.
  • open_questions: empty. Nothing to answer.
  • out_of_scope_findings: the bare plain read of two writable pairs answering 200 is carried by metadata-protocol: a name held only by stored rows of two enabled packages answers the bare read by save order while /layers refuses 409 (ADR-0150 D3) #32607 (triage 6867282696 agrees). Escalated to the seat: the /diff door still diffing two holders' versions as one item's, and @objectstack/client's getHistory sending no package, are carried only by this PR's body. A PR body stops being a carrier at merge, so each needs a card or a line on an open one.
  • Deviation "patch with BREAKING banner and Metadata-protocol none": refused by the gate (F1).

④ Design judgment

packages/spec/src/api/protocol.zod.ts against ADR-0150 D3 to D5 and its first consumer (the history route). The field is optional and additive, and its description matches what historySlotOf does for a slotted type, row-slot wording included. Two sentences do not hold against the consumer: "Every other type reads the name's log either way" publishes the ignored selector of F4 as the contract, where the neighbouring member selector is "Refused with 400 VALIDATION_ERROR on a type that has no members"; and "its own row's, or its .custom pair's declared override's" omits the two arms the code adds (the package's own empty slot when it only ships the name, and the 409 not-held-by-qualifier). The rollback door has no request schema in the spec, so its ?package= is declared nowhere in the published contract. That is pre-existing for that door and graded nit.

Implementing files, tried against the brief's four attacks.

Can a bare rollback still write one holder's row when two hold the name? Not when both hold a stored active row, whether two pairs or a package beside the package-less row: refused 409 before anything is read. A holder whose row is inactive or deleted is not a slot, so the name is then one-slot and resolves bare; the removed holder's attributed versions are refused by versionOf. One path remains:

  • F3 · should-fix · a bare request still resolves when the second holder holds the name in code only. rowSlotsOf reads packagelessAddress, which counts stored active rows. Two packages ship flow X, and only alpha's .custom pair has saved an override: one slot, so the bare history answers 200 and the bare rollback writes alpha's pair, while the bare GET /meta/flow/X of the same name is refused 409 naming both. ADR-0150 D5: "A bare address resolves while the name has one holder in the environment, and answers 409 … otherwise". The write lands in the only row that has versions, and those are alpha's own, so no body crosses packages; it is the ADR's URL rule and red line 2 that are not met. Refuse on the holder set the bare read uses (stored rows plus shippingPackageIds), or narrow the claim and name the residual on a card.

Does ?package= let a caller roll back another package's version? No. versionOf carries the addressed slot, another slot's attributed version answers 404 before the body is parsed, and an unattributed one is admitted only while the slot is the name's sole slot. One gap beside it:

  • F6 · should-fix · with no stored row of the name, ?package= is taken at its word. In historySlotOf the not-held-by-qualifier refusal requires slots.length > 0. With zero rows, any package id, installed or not, becomes the slot with unattributed: true, so a rollback to a pre-release version of a deleted item is written into a new row bound to whatever package the caller spelled. Between historySlotOf and restoreVersion the rollback path asks packagelessOverlayWriteRefusal and namedBaseOverlayWriteRefusal only; the save door's uninstalledNamedBaseRefusal is not on it. DatabaseLoader.rollbackSlot answers "no slot" here. Read from the code, not run, and no test in the diff covers it. Refuse a package that neither held the version nor ships the name, or show the gate that already does.

Does writing package_id break re-runnability or old rows? No: nullable column, no backfill, NULL stays "unattributed", and an unattributed version stays listed and restorable while the name has one slot. Note for the fix round: historyRowAttribution falls back to the package a NULL-column row's stored body names (storedItemPackageId: the envelope packageId or a _packageId stamp), so a row saved before #16702 stripped those stamps is attributed by its body, not "unattributed". Nit: the restoreVersion comment "sys_metadata_history carries no package_id column, so a vanished binding is not recoverable" is now false in a file this diff edits.

Does a one-holder name's bare history and rollback answer exactly as on main? History: yes (versionsOf is passed only when the request named a package). Rollback: no.

  • F2 · must-fix · the bare rollback of a one-slot name now refuses versions main restores (red line 3). rollbackMetaItemUnaudited passes versionOf whenever slot is defined, the bare one-slot case included, as { packageId: rollbackPackageId, unattributed: true }. isVersionOf then requires an attributed version to equal the slot. So once this release attributes rows: a flow saved package-less (versions marked sys_metadata), whose row is then deleted and saved again under a package (or taken over by one, the case os migrate packageless-overlays produces), has one holder, and its bare rollback to any earlier version answers 404 "belongs to the environment's own row". On main that request restores. The PR says it keeps "the one [rule] DatabaseLoader already keeps", but DatabaseLoader.rollbackSlot carries exactly this case as M1: "The environment's own version of a name one row slot holds lands in that row, as before (M1): a package took over the package-less row, or it was removed and a package saved since". The two readers of one table now disagree. The bare history of the same name still lists those versions, so the console offers a rollback the door refuses. Carry M1 into isVersionOf for a sole slot, add the CONTROL for it, and correct the changeset sentence. If the refusal of a removed package's version on a sole slot (the loader's S8a) is meant to apply here too, declare it as a narrowing.

Agreement with the contract, remaining sentences.

  • F4 · should-fix · on a type outside PACKAGE_SLOTTED_STORE_TYPES, ?package= is accepted and dropped, on a write door. historySlotOf returns undefined, and the rollback then takes the bare path whatever the caller named. POST /meta/view/NAME/rollback?package=B was a located 400 on main; it now answers 200 and restores the row the bare read serves. That is the shape route rule 5 exists to prevent ("a dropped filter returns the full set"), and a request that was refused now serves, against the PR's own red-line-1 sentence "Nothing that refused before serves now". Refuse it 400 on a type the door does not address by package, as member is on a memberless type, or honour it.
  • F5 · should-fix · ?package=P does not address the row the read naming P serves when that row is the package-less one. The module header says the slot is "the slot of the row a read naming P serves". findServedOverlayRow serves the package-less row as P's fallback (servedOverlayRowCandidates lists { packageId: null } after the package's own row), and historySlotOf then discards that binding and takes P itself. For an unmigrated package-less overlay of a flow P ships: P's slot is empty and not sole, so the history is an empty list and every rollback 404s; when P does not ship the name it is 409 not-held-by-qualifier. On main the console's bare call listed the whole log. The console now sends the item page's ?package= from all three mounts, and package-scoped lists show package-less rows as stand-ins, so the History tool goes empty or errors for items it showed before. Read from the code, not run. Address the served row's slot (the environment's), or answer with the holder to name.
  • Nit: the protocol.ts and protocol-door-requests.ts docblocks say "the version must be that holder's: another holder's answers 404" without the unattributed arm that sys-metadata-repository.ts states.
  • meta-item-history-routes.ts, rest-server.ts, metadata-client.ts, ResourceHistoryPage.tsx, StudioItemTools.tsx, ResourceEditPage.tsx, database-loader.ts, index.ts, sys-metadata-history.object.ts, api.json: agree with the contract apart from the findings above. handleRouteError with packageScopedCandidates renders the holders as the by-name reads do.

Findings: F1 must-fix (red Check Changeset: Metadata-protocol: added, @objectstack/spec minor) · F2 must-fix (bare one-slot rollback refuses what main restores; M1 missing) · F3 should-fix (code-only second holder resolves bare) · F4 should-fix (ignored ?package= on non-slotted types) · F5 should-fix (package-less fallback row not addressed by ?package=P) · F6 should-fix (zero-row name binds to any spelled package) · three nits (rollback request undeclared in spec, stale restoreVersion comment, docblock arm). A delta review is owed on the fix head.

Implemented-by: claude/project-thread-9jncd7
Reviewed-by: session_01L9qibzECS2npZFku4956Um

VERDICT: FAIL

…does

Bare history and rollback now also refuse where the bare plain read
refuses (a second holder in code counts), and a bare rollback of a
one-slot name restores what main restores. `?package=P` addresses the
slot of the row a read naming P serves, including the package-less
fallback; with no stored row an uninstalled package is refused like a
save naming it. `?package=` on a type not stored per package is refused
400 on both doors. The rollback request gets a spec schema.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
…nd which answer as before

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
… collection in the slot pins

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
@github-actions github-actions Bot added size/xl and removed size/l labels Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibzECS2npZFku4956Um

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d7d23db10859f38b7ab22cec661687c92e8c8a93
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/rest/src/meta-item-history-routes.ts, packages/rest/src/rest-server.ts, packages/metadata-protocol/src/protocol-history-slots.ts, packages/metadata-protocol/src/protocol-history-and-publish.ts, packages/metadata-protocol/src/protocol-door-requests.ts, packages/metadata-protocol/src/protocol-package-commits.ts, packages/metadata-protocol/src/protocol-package-scope.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/sys-metadata-repository.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/index.ts, packages/metadata-core/src/objects/sys-metadata-history.object.ts, packages/spec/authorable-surface/api.json, packages/spec/api-surface/api.json, packages/spec/declaration-map/api.json, packages/spec/export-origins/api.json, packages/spec/json-schema.manifest/api.json, ui/packages/data-objectstack/src/metadata-client.ts, ui/packages/app-shell/src/views/item-tools/ResourceHistoryPage.tsx, ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx, ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx

Delta review, rendered 2026-11-14T04:03Z, of the patch round on record 6869129898 (head 1c1a28f9, FAIL: F1 and F2 must-fix, F3 to F6 should-fix, three nits). Read: card #32905 and all its comments (none new since the os-dev-report 6869029675), the replaced PR body, the net diff against the merge-base with main (f45c0125), the round's own delta (1c1a28f9..d7d23db1, less the main merge), and the check-runs on this head. Code outside the diff was read, not reviewed. Tests, changeset prose and content/docs stay the lane seat's ACCEPT at TIER_DEFAULT. No UI file changed in this round.

Check-runs on this head when judged. Check Changeset ran twice: run 132323607119 (started 03:58:46Z) failed reading the PR body before it was replaced (it still saw Metadata-protocol: none); run 132323861521 (started 03:59:49Z, after the edit) is success, and the newest run governs. Success: Dogfood Regression Gate, Spec property liveness, Governed Surface Queue Guard, PR-body guards, Check Documentation Links, Check PR Size, Flag docs affected by code changes. Still running, not judged: Test Core 1/8 to 8/8, Build Core, Build Docs, Build UI Deps, Live E2E UI, Lint · ESLint, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · repo & tooling gates, the three Lint · scoped runs, Type Check · source gates, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger; the Test UI shards had not been created yet. This PASS is on the tree as read; a merge waits for those runs.

① Derived judgments

  • Bare 409 on both doors. Right, and now on the holder set the bare plain read judges: two row slots (ambiguous-in-environment), or registry.resolveItem(type, name, null, member) refusing, which is the very call readMetaItem makes for a read naming no package. A name that read serves is not refused here, so nothing a one-holder name answered on main moves.
  • Bare rollback of a one-slot name. Right: versionOf is passed only when the request named a package, so the bare restore is main's, any version of the name.
  • ?package= on a slotted type. Right: the slot of the row findServedOverlayRow serves, the package-less fallback included (null, the environment's slot); else the package's own slot.
  • ?package= on any other type. Right: 400 VALIDATION_ERROR (param: package) from refusePackageSelectorOutsideSlottedTypes, asked on both doors ahead of their early answers. It was 400 on main too (unknown parameter), so nothing that refused now serves. all and the empty string still fold to no package through metaItemPackageBinding, as ruled for every /meta door, and the changeset says so.
  • 404 for another slot's version; unattributed and environment-attributed versions belong to a sole slot only. Right, and now the same rule DatabaseLoader.rollbackSlot keeps (M1).
  • 422 WRITABLE_PACKAGE_REQUIRED for an addressed rollback into a slot no row holds, naming a package that is not installed. Right: the save door's own uninstalledNamedBaseRefusal, with a rollback sentence.
  • sys_metadata_history.package_id, the two @objectstack/metadata exports, the client's packageId. Unchanged from the first review, right.
  • HistoryMetaItemRequestSchema.packageId and the new RollbackMetaItemRequestSchema. The shared description now states every arm the code has. The generated shards agree with the source: seven authorable-surface lines (one History key, six Rollback keys), three api-surface names, the declaration-map, export-origins and manifest entries. One member of the new schema is declared wider than the door (N1).

② Semver level

  • F1 answered. PR body and changeset both read Clause-②: yes (narrowing) and Metadata-protocol: added, and @objectstack/spec is minor. The level axis of the newest Check Changeset run accepts it. added is the right kind: keys entered the authorable surface and none left.
  • patch for metadata-core and app-shell is right. minor for rest, metadata-protocol, metadata and data-objectstack is not refused by the gate; under ruling A ("the level follows the METADATA PROTOCOL alone … REST, drivers, services, the console → patch") they could be patch with the BREAKING banner. Over-grading harms nothing; nit, the seat's call.
  • ADR-0087 entry: not owed. check-adr-0087-registration accepted the reworded not-required (no-migration-prescription) marker in the green run.
  • The changeset's one-holder sentence is now true of the tree (F2).

③ Boundary flags

  • The first review's surface answers stand (rest-server.ts closed sets, the spec file with its generated shards, the metadata-core doc line, one prop each in StudioItemTools.tsx and ResourceEditPage.tsx).
  • New surface this round: protocol-package-scope.ts (one verb added to uninstalledNamedBaseRefusal's message), accepted as F6's fix; four more generated spec shards, accepted as the consequence of the new schema.
  • open_questions: none raised on the card or in the PR body this round.
  • Seat-held, unchanged: the /diff door still diffing two holders' versions as one item's, and @objectstack/client's getHistory sending no package. The PR body now says the seat keeps both. The bare plain read of two writable pairs stays with metadata-protocol: a name held only by stored rows of two enabled packages answers the bare read by save order while /layers refuses 409 (ADR-0150 D3) #32607.

④ Design judgment

packages/spec/src/api/protocol.zod.ts against ADR-0150 D3 to D5 and its first consumer. HISTORY_PACKAGE_SELECTOR_DESCRIPTION now holds sentence by sentence against historySlotOf and isVersionOf: the served row's slot with the fallback, the empty own slot, not-held-by-qualifier, the sole-slot rule for unattributed and environment versions, the bare 409 "in stored rows or in code", the 400 on other types.

  • N1 · should-fix · RollbackMetaItemRequestSchema.toVersion is published as z.number(), and the door accepts a positive integer only. The REST door judges the body through UNDECLARED_WHOLE_NUMBER_PARAM and refuses below 1 ('toVersion' (positive integer) is required); the protocol method refuses the same (requires a positive integer 'toVersion'). The new schema declares 1.5, 0 and -1 acceptable, and nothing reads it, so the published contract and the door now spell the version two ways. The neighbouring HistoryMetaItemRequestSchema.limit explains its missing bounds by "nothing on this path clamps or refuses"; here something does. Declare what the door enforces (a whole number of at least 1), and preferably have the door read its declaration in place of the UNDECLARED_ constant, or say on the schema why not. Nothing served changes either way, which is why this is not a must-fix.

Each finding of record 6869129898, against the code.

  • F2 (must-fix) answered. rollbackMetaItemUnaudited passes versionOf only for slot?.addressed. HistoryVersionOwner.unattributed became sole, and isVersionOf admits an unattributed or an environment-attributed version into a sole slot. A name saved package-less and then taken over by a package restores its earlier versions bare (no check, as main) and addressed (M1). Another package's attributed version stays refused on an addressed rollback, which is the loader's S8a.
  • F3 (should-fix) answered. refuseWhereBareReadRefuses asks the registry's package-scoped resolver, only for a type the registry reads by package, and rethrows only a package-scoped refusal. It runs for zero and one row slots alike, so a name two packages ship with no stored row is refused bare too; that follows "wherever the bare plain read refuses", though the changeset's list of shapes names only the one-row case (nit). For action the plain read also asks two judges of its own (refuseAmbiguousObjectLessAction, judgeBareActionRead) that this door does not; whether resolveItem covers every case they refuse was not established from the read, and no pin in the diff uses an action (nit, worth one line on the card).
  • F4 (should-fix) answered. Refused 400 at the protocol on both doors, before the overlay-allowed early answer of each.
  • F5 (should-fix) answered. slot is the served row's binding, null for the package-less fallback, and packageId only when the read serves nothing. Consequence, consistent with the read and accepted: a package that neither holds nor ships the name addresses the environment's slot when a package-less row is what its read serves, and the rollback then meets the package-less write gates the bare path meets.
  • F6 (should-fix) answered. HistorySlot.held is false for a slot no stored row holds, and the rollback then asks uninstalledNamedBaseRefusal before any write. An installed package naming a zero-row name restores an unattributed or environment version into its own new row, as a save naming it would create one; a version attributed to another package is still 404.
  • Nits answered. The rollback request is declared (N1 on its one loose member), the restoreVersion comment is corrected, and the protocol.ts and protocol-door-requests.ts docblocks carry the unattributed and environment arm.

Red line 3, tried again on this head. A one-holder name, bare: history unfiltered, rollback unchecked, both as main. The only new bare refusals are two row slots and the plain read's own refusal, and neither is a one-holder name. ?package= was refused on main for every type, so no addressed answer can differ from a main answer that served. Writing package_id still rewrites nothing, and NULL rows keep their place under the sole-slot rule. No new defect found in the fixes beyond N1.

Findings on this head: N1 should-fix (toVersion declared wider than the door) · three nits (four packages graded minor where ruling A allows patch; the zero-row two-shipper shape missing from the changeset's list; action's extra bare-read judges not asked, unverified). F1 to F6 and the first review's nits: answered. A delta review is owed on N1's fix head.

Implemented-by: claude/project-thread-9jncd7
Reviewed-by: session_01L9qibzECS2npZFku4956Um

VERDICT: PASS

… number of at least 1

The rollback request's spec schema now declares `toVersion` as the door
enforces it, and the door reads that declaration in place of an
undeclared whole-number parse. The changeset grades every package but
spec `patch` under the breaking banner, names the zero-row two-shipper
refusal, and states what the bare doors do not yet ask for an action.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Contract-review-claim: session_01L9qibvyhxPWfA8DsbmVYJ5 · head 248bc6b · 2026-11-14T16:15Z

The delta review of M1 owed by record 6878813627, by the same isolated reviewer that rendered it.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 248bc6beca06c010dc173348b8fcc7fb20eceec4
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/metadata/src/package-slotted-store.ts, ui/packages/app-shell/src/views/item-tools/ResourceHistoryPage.tsx, ui/packages/data-objectstack/src/metadata-client.ts, ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx, ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx, packages/metadata-protocol/src/protocol-history-slots.ts, packages/metadata-protocol/src/protocol-history-and-publish.ts, packages/metadata-protocol/src/protocol-package-commits.ts, packages/metadata-protocol/src/protocol-door-requests.ts, packages/metadata-protocol/src/protocol-package-scope.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/sys-metadata-repository.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/index.ts, packages/metadata-core/src/objects/sys-metadata-history.object.ts, packages/rest/src/meta-item-history-routes.ts, packages/rest/src/rest-server.ts, packages/spec/api-surface/api.json, packages/spec/export-origins/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, packages/spec/json-schema.manifest/api.json

Delta review of the M1 round owed by record 6878813627 (FAIL on abe8a83cb: M1 must-fix, two nits). Read: card #32905 and its six comments (none new since the release 6872649647); PR #33193's re-patched body, its 35-file list and the net diff against the merge-base with main (e540baf88); the round's own delta abe8a83cb..83f41ed6d (one commit, ten files) and what the main merge 248bc6bec brought beside it; the check-runs on this head. reviewFace still returns packages/spec/src/api/protocol.zod.ts alone as hits. Code outside the diff was read, not reviewed. Tests, the changeset prose and content/docs are the lane seat's ACCEPT at TIER_DEFAULT. Nothing was built or run.

Check-runs on this head when read (16:18Z, 49 runs created). Success: Check Changeset (both runs), Dogfood Regression Gate, Spec property liveness, Governed Surface Queue Guard, PR-body guards (run 132506834415; its earlier twin 132506657323 was cancelled and superseded), Check Documentation Links, Check PR Size, Flag docs affected by code changes, Auto Label, Plan Test Core shards, Plan Test UI shards, filter. Failed: none. Still running, not judged: Test Core 1/8 to 8/8, Build Core, Build Docs, Build UI Deps, Live E2E UI, Lint · ESLint, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · repo & tooling gates, the three Lint · scoped runs, Type Check · source gates, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger. Not created yet: the Test UI shards, Lint UI, Type Check UI, Build & E2E UI. The round's own pins (the console tests and the list pin in protocol-history-slots.test.ts) sit in runs that have not reported. This PASS is on the tree as read; a merge waits for those runs, and a red one among them reopens the round.

① Derived judgments

② Semver level

  • Clause-②: yes (narrowing) and Metadata-protocol: added stand on the PR body and the changeset alike, and both Check Changeset runs on this head are success.
  • The new spec exports are graded and declared right. @objectstack/spec is minor, which is what two added exports need, and the changeset and PR body both name them. They are additions only; nothing published is removed or re-shaped, so the adr-0087: not-required marker still holds.
  • @objectstack/metadata stays patch: its export keeps its name, type and members. @object-ui/app-shell and @object-ui/data-objectstack stay patch: a behaviour fix and two docblocks.
  • The changeset's console paragraph now says what the tree does: the History tool names the package for a type stored per package only.

③ Boundary flags

④ Design judgment

packages/spec/src/api/protocol.zod.ts against its two consumers. The list's docblock holds sentence by sentence: the doors take ?package= on these types and refuse it 400 on every other (refusePackageSelectorOutsideSlottedTypes asks PACKAGE_SLOTTED_STORE_TYPES, now this list); @objectstack/metadata's set is built from it; the console names a package only where the predicate answers true. HISTORY_PACKAGE_SELECTOR_DESCRIPTION is unchanged and its last sentence is no longer broken by the console. The RollbackMetaItemRequestSchema docblock now names the version alias.

  • M1 (must-fix of record 6878813627) answered. Tried again: an object, page, view, dashboard, app or report opened under a package sends a bare history and a bare rollback, which is the request main sends, so the doors answer as on main; a flow opened under a package still names it, in the singular or the plural spelling. The console cannot drift from the doors by a second copy of the list, because there is none: the door's set and the console's predicate are one array. The MetadataHistoryOptions.packageId and rollback() docblocks in metadata-client.ts now say the selector is refused 400 outside by-package types and name the predicate. The client itself still forwards whatever packageId it is handed, which is right for a transport: the door's 400 is the contract, and the caller that broke it is fixed.
  • The pins. The console half is pinned on a mocked client (an object under ?package= reads and restores bare; flows still names the package; Studio's object and page pillars read bare and its flow pillar names the package), and protocol-history-slots.test.ts holds the predicate and the door's refusal to one list in both directions. No test drives the console against the real door, but with one list by construction the two halves can no longer disagree the way they did; accepted.
  • Red line 3 from the console. A type not stored per package: bare at both doors, as main. A by-package type opened with no package: bare, as main, and refused only where two hold the name. A by-package type opened under a package is the one request that differs from main (it names the holder), and it is the change the card asks for: for a one-holder name the addressed slot is sole, so its own, unattributed and environment versions are listed and restorable, and only a version attributed to another package is withheld, the narrowing the changeset declares. Server side, nothing moved since the last record, and the main merge leaves the bare path as judged there.
  • Nits of record 6878813627. The protocol method's Number.isFinite check is outside the diff and left as an accepted deviation; the docblock nit is fixed.
  • Nit: the file header of ResourceHistoryPage.tsx still reads "naming the item page's ?package= when it has one", without the by-package condition the panel's own docblock now states.
  • Nit: the list is compiled into the console, so a console built from a newer spec than the server it talks to would name a package for a type that older server still refuses. The two ship together today; no effect on this head.
  • The remaining implementing files (protocol-history-slots.ts, protocol-history-and-publish.ts, protocol-package-commits.ts, protocol-door-requests.ts, protocol-package-scope.ts, protocol.ts hunks of this PR, sys-metadata-repository.ts, database-loader.ts, index.ts, sys-metadata-history.object.ts, meta-item-history-routes.ts, rest-server.ts, StudioItemTools.tsx, ResourceEditPage.tsx): unchanged by the round and agree with the contract as the earlier records judged them.

Findings on this head: no must-fix, no should-fix · two nits (stale file-header sentence in ResourceHistoryPage.tsx; the console's compiled-in list under version skew). M1: answered. The escalation in ③ (three follow-ups without a card) stands and is the seat's to close before the merge. Nits alone end the round; the merge waits for the check-runs still running above.

Implemented-by: claude/project-thread-9jncd7
Reviewed-by: session_01L9qibvyhxPWfA8DsbmVYJ5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Regen-provenance: 6880527562 · 248bc6beca06c010dc173348b8fcc7fb20eceec4 → 8a633e802d9819491de6bab0817d08e8c64006d0 · git diff --name-only 248bc6bec 8a633e802 -- $(git diff --name-only origin/main...HEAD) → (empty)

A merge-forward of origin/main only (K was 19 against PRBASE e540baf88), run with scripts/pm/os-regen-merge.sh: none of the PR's 35 paths moved across the hop, the three-dot diff against main is byte-identical apart from index lines (35 files, +1498 / −70), and pnpm --filter @objectstack/spec check:generated answers 0 on the merged tree after a spec build. Delta review PASS 6880527562 carries to this head; the PR lands once every check on it is green.

Follow-ups the review asked for are filed: #33308 (diff door), #33310 (SDK getHistory / rollbackItem / diffItem), #33311 (action bare doors).

2026-11-14T16:28Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Landing

Head-sha: 8a633e802d9819491de6bab0817d08e8c64006d0

Ready + auto-merge now.

2026-11-14T17:02Z

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 43619123356 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (4/8) — 失败步骤: Run this shard's tests

    @objectstack/objectql:test:  FAIL   local  src/protocol-custom-override-bare-slot-envelope.test.ts > [#33131] two namespaces ship one name, one declares its override: each holder's address serves its 
      ↳ 失败原因: @objectstack/objectql:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/protocol-custom-override-bare-slot-envelope.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Merge-queue red: triage

Queue build 43619123356 (tree e5c5e1f0a, this PR on main 6538ba48f), Test Core (4/8), step "Run this shard's tests".

Signature: packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts › [#33131] two namespaces ship one name, one declares its override … › alpha registered first, alpha and beta overridden: the read naming no package refuses 409 … — first error Error: Test timed out in 5000ms. at :949:13. A timeout, not an assertion. New signature (the triage comment counts no other PR on it in 24 h). The file took 63.5 s in that shard; 109 of its 110 cases passed.

First diagnosis: load, not this PR. The file's import closure does meet this diff (@objectstack/metadata-protocol, @objectstack/metadata-core), so the re-enqueue-once exemption does not apply as written, and this was measured instead. The same file, back to back on one machine, with only the three packages' sources swapped:

Sources Tests time (s)
main's packages/{metadata-protocol,metadata-core,metadata}/src 13.26, 11.48
this PR's 12.56, 12.19

The failing case runs in about 0.5 s alone, both ways. Each case of that describe boots the engine, installs two or three packages and restarts, under the 5 s default; on a loaded shard that is the margin that gave out.

Disposition: not re-enqueued blind. The fix is in this PR: that describe gets { timeout: 30_000 } with a comment carrying the measurement (test-only, no product change). The new head gets a delta review before it goes back into the queue.

2026-11-14T17:33Z

…d shard

Each case of that describe boots the engine, installs two or three
packages and restarts it: about half a second alone, but a merge-queue
shard timed one out at the 5 s default while the file took 63 s. The
file's runtime is the same on main's sources and on this branch's,
measured back to back, so the budget is room for load, not for a
slower product.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvyhxPWfA8DsbmVYJ5
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 45dca14ce18d799a18b37236eadcb7101e04ee37
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/metadata/src/package-slotted-store.ts, ui/packages/app-shell/src/views/item-tools/ResourceHistoryPage.tsx, ui/packages/data-objectstack/src/metadata-client.ts, ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx, ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx, packages/metadata-protocol/src/protocol-history-slots.ts, packages/metadata-protocol/src/protocol-history-and-publish.ts, packages/metadata-protocol/src/protocol-package-commits.ts, packages/metadata-protocol/src/protocol-door-requests.ts, packages/metadata-protocol/src/protocol-package-scope.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/sys-metadata-repository.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/index.ts, packages/metadata-core/src/objects/sys-metadata-history.object.ts, packages/rest/src/meta-item-history-routes.ts, packages/rest/src/rest-server.ts, packages/spec/api-surface/api.json, packages/spec/export-origins/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, packages/spec/json-schema.manifest/api.json

Delta review of the hop 8a633e802 → 45dca14ce, on top of PASS record 6880527562 (head 248bc6bec) and its Regen-provenance carry 6880677176 (to 8a633e802). Read: record 6880527562 in full; the carry 6880677176; the queue triage 6881435910 and the seat triage 6881523326; the log of queue job 132521325013 (run 43619123356, Test Core 4/8); git show 45dca14ce; the whole changed describe in packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts (lines 782 to 1020); the check-runs on both heads. The carry was re-derived, not trusted: 8a633e802 has parents 248bc6bec and main's 24f2f4014, and git diff --name-only 248bc6bec 8a633e802 over the PR's 35 paths is empty. The hop is one commit, one file, +5 / −1. That file is a test, so it sits outside the Scope line above and is normally the lane seat's ACCEPT; it is judged here because the seat asked for a delta review before the PR goes back into the queue. Nothing was built or run; the seat's local timings were read, not reproduced.

Check-runs on this head when read (17:57Z, 37 runs created). Success: Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, PR-body guards, Spec property liveness, filter. Skipped: nine opt-in or queue-only runs (Queue verdict, Borrowed queue verdict, Schedule gate, Packed-tarball smoke, 18.x upgrade harness, Dogfood Verify CLI, the Dogfood matrix placeholder, Temporal Conformance, UI Published Artifacts). Failed: none. Still running, not judged: Build Core, Build Docs, Build UI Deps, Check Changeset, Flag docs affected by code changes, Live E2E UI, Lint · ESLint, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · repo & tooling gates, the three Lint · scoped runs, Plan Test Core shards, Plan Test UI shards, the four Type Check runs. Not created yet: the Test Core and Test UI shards. So the changed file has not run on this head. This PASS is on the tree as read; a red run among those reopens the round.

① Derived judgments

  • The hop touches no file of the Scope line and no product code. git diff --name-only 8a633e802 45dca14ce names one path, the test file. That file is not one of the PR's 35 paths either: it reached the branch from main (fix(metadata-protocol)!: when two packages ship one name and one declares its .custom override, each package's slot serves its own item (ADR-0150 D4) #33189). The PR now has 36 files; the review face is unchanged, packages/spec/src/api/protocol.zod.ts alone.
  • The change is a timeout widening only. The diff adds four comment lines and puts { timeout: 30_000 } as the second argument of one describe. No it, expect, loop bound, fixture or helper moved. The file has no .skip, .only, .todo or retry, before or after. The same 19 cases of that describe run, with the same assertions; the other 91 cases of the file keep the 5 s default. describe(name, options, fn) is a supported form on the pinned vitest (4.1.12), and a suite's timeout is inherited by its tests.
  • The failure was a timeout, not an assertion. The job log reads Error: Test timed out in 5000ms. at :949:13, the it( line of the bare-read case; 110 tests, 1 failed, file time 63548 ms. Both figures match what the two triage comments and the new comment say.
  • Nothing published changes. No schema, no export, no generated shard, no changeset and no doc is in the hop.

② Semver level

  • No change. The hop is test-only, so the levels and the Clause-②: yes (narrowing) and Metadata-protocol: added lines judged in record 6880527562 stand as they were.
  • No changeset is owed for a test file. Check Changeset is still running on this head and is not judged.

③ Boundary flags

④ Design judgment

packages/spec/src/api/protocol.zod.ts and every implementing file of the Scope line: byte-identical to the tree record 6880527562 passed, across both hops. Nothing to re-judge.

The hop, tried four ways.

  • Is load the plausible cause? Yes, and the queue log shows it without the seat's local numbers. In that shard the 19 cases of this describe took 0.6 s to 4.5 s each and about 50 s together, in a file that the seat times at about 12 s whole. Three passing siblings were already at 3.7 s, 3.9 s and 4.5 s. The package run reports 1935 s of import time inside 818 s of wall time, so the shard was heavily shared. The failing case did not hang: it was cut at 5198 ms, and its mirror case (beta registered first, alpha and beta overridden), which does the same work in the other order, passed in 1791 ms in the same run.

  • Could 30 s hide a hang or a slowdown this PR brought? A true hang still fails at 30 s, so it is not hidden. A slowdown from this PR is what the seat measured against: the file took 13.26 s and 11.48 s on main's sources of the three packages, and 12.56 s and 12.19 s on the PR's. Two runs a side is a small sample, but the two ranges overlap and the gap the queue showed is five to ten times, not ten percent. A second, independent reading agrees: Test Core (4/8) and the other seven shards were green on 8a633e802, the same product sources, on the PR side. The evidence is adequate for a test-budget change. It was not reproduced here.

  • Is 30 s a fair size? The slowest passing case in the loaded shard was 4.5 s and the cut one would have needed a little over 5 s. 30 s is about six times that. It is generous, but it is scoped to one describe and it cannot turn a wrong answer green: every assertion still runs.

  • Does the new comment say anything false? One clause is loose. See the nit below. The run number, the 5 s cut, the 63 s file time and the two timing ranges all match the log and the seat's table (11.48 to 13.26, 12.19 to 12.56).

  • Nit: the comment says each case "boots the engine, installs two or three packages and restarts it". Ten of the 19 cases boot once and never restart, and the case that timed out is one of them (it boots, publishes 18 overrides and reads the bare address three times). The last control installs one holder. "Each case boots the engine with one or two holders and their pairs; about half restart it" would be exact. The commit message repeats the same clause. No effect on the test or on the reason given.

  • Nit: "about 0.5 s alone" and the two timing ranges are the seat's local readings. The file cannot show their source; the comment already names the queue run, which is the checkable half.

Findings on this head: no must-fix, no should-fix · two nits (the "restarts it" clause over-states what ten of the cases do; the local timings rest on the seat's word). The PASS of record 6880527562 stands on 45dca14ce. The merge waits for the check-runs still running or not yet created, the Test Core shard that runs this file first among them.

Implemented-by: claude/project-thread-9jncd7
Reviewed-by: session_01L9qibvyhxPWfA8DsbmVYJ5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Regen-provenance: 6881836907 · 45dca14ce18d799a18b37236eadcb7101e04ee37 → 952550aea38aba428e045206b01aedc104f49090 · git diff --name-only 45dca14ce 952550aea -- $(git diff --name-only origin/main...HEAD) ':!packages/spec/authorable-surface/api.json' → (empty)

A second merge-forward of origin/main (K had reached 34 while the fix ran CI), run with scripts/pm/os-regen-merge.sh, plus one regeneration commit. The script took main's side of packages/spec/authorable-surface/api.json (both sides had moved it) and its hook refused the hand-off until pnpm --filter @objectstack/spec gen:schema re-derived it; the regenerated file keeps this PR's seven HistoryMetaItemRequest / RollbackMetaItemRequest entries and gains main's. No other of the PR's 36 paths moved. After a spec build, pnpm --filter @objectstack/spec check:generated answers 0. Delta PASS 6881836907 carries to this head; the PR re-lands once every check on it is green.

2026-11-14T18:33Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibvyhxPWfA8DsbmVYJ5

Re-landing after the queue red

Head-sha: 952550aea38aba428e045206b01aedc104f49090

  • Queue red handled: triage 6881523326 (timeout under load in packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts, A/B-measured as not this PR); fixed in 45dca14ce with a 30 s budget on that describe, no case or assertion changed.
  • Review: delta PASS 6881836907 on 45dca14ce, carried to this head by Regen-provenance 6882296432 (merge of main plus a regeneration of packages/spec/authorable-surface/api.json; no other PR path moved).
  • Checks: 59 success, 9 skipped, 0 failed, 0 running; check-expected-skips.mjs --pr 33193 OK.
  • Mergeable: clean. K = 7.
  • Tier: NOT governed; 1574 changed lines (≤ 3000).

Auto-merge again.

2026-11-14T19:05Z

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit b462444 Nov 14, 2026
68 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-9jncd7 branch November 14, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants